Skip to content

feat: warn on unmerged sync PR and add --direct-merge#13

Merged
flipvh merged 1 commit into
mainfrom
feat/sync-preflight-guard-direct-merge
Jul 9, 2026
Merged

feat: warn on unmerged sync PR and add --direct-merge#13
flipvh merged 1 commit into
mainfrom
feat/sync-preflight-guard-direct-merge

Conversation

@flipvh

@flipvh flipvh commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Why

Running pnpm cella sync again before merging the previous sync PR silently stacks changes: the last-sync point lives in cella.manifest.json committed on the sync branch, so a new cycle cut from a trunk that still lacks the merged PR re-includes that PR's entire delta on top of the new upstream commits — the "why are there suddenly so many changes" surprise.

Two tweaks address this.

1. Pre-flight guard for an unmerged sync PR

Before cutting a fresh sync branch, cella sync now checks for an open sync PR (head branch under cella/sync/) via gh and prompts:

⚠ an earlier sync PR is still open and unmerged:
  #42  chore: sync upstream cella v0.3.0 (abc1234)
         https://github.com/org/app/pull/42
  starting a new sync now produces a PR that re-includes those changes.
? how do you want to proceed?
  ❯ merge #42 first, then sync   (recommended)
    continue anyway   (new PR will re-include the unmerged changes)
    cancel
  • merge firstgh pr merge --squash --delete-branch the open PR, then fast-forward the local trunk so the new cycle cuts clean. A PR GitHub refuses to merge (conflicts / failing required checks — the "breaking changes" to fix first) stops the run with guidance.
  • continue → today's behaviour.
  • cancel → bail.

Multiple stacked sync PRs: the newest is a content superset, so it is merged and the older ones are closed.

Degrades to a silent no-op when it can't help: no gh, no open sync PR, or a non-interactive session (no TTY).

2. --direct-merge flag

Once the sync PR is open, enables GitHub auto-merge (squash) so it merges itself as soon as required checks pass — no forgotten open PR to trip over next time. Opt-in per run (deliberately not a config default — a sync PR is usually worth a human look), and needs auto-merge enabled on the repo. Falls back to printing the manual command if the repo does not allow it.

release-please is unaffected either way: it watches the trunk for the single squashed chore: sync upstream cella … commit, which lands identically whether the PR is merged by hand, by the guard, or by auto-merge (the branch is already flattened to one commit before pushing).

Implementation

  • New src/utils/gh.ts consolidates the scattered gh calls behind small wrappers (ghAvailable, listOpenSyncPrs, mergePrSquash, closePr) with pure, unit-tested cores (filterSyncPrs, buildMergeArgs).
  • src/services/sync.ts: guardAgainstOpenSyncPr / mergeOpenSyncPrs in the fresh-cycle path; enableAutoMerge in shipSyncBranch.
  • --direct-merge wired through cli.ts + RuntimeConfig.

Tests

  • tests/gh.test.tsfilterSyncPrs (prefix boundaries) and buildMergeArgs (flag combinations).
  • tests/cli.test.tssync --direct-merge parsing.
  • Full suite green (14 files, 144 tests); pnpm ts + pnpm biome check clean; verified sync --help lists the flag after pnpm build.

The gh-network paths degrade to a no-op without gh, which is exactly the e2e condition, so the existing e2e suite is untouched.

🤖 Generated with Claude Code

Two tweaks to `cella sync` to stop forgotten sync PRs from piling up.

Pre-flight guard: before cutting a fresh sync branch, check for an
open sync PR (head branch under `cella/sync/`) via `gh`. Because the
last-sync point lives in the manifest committed *on the branch*, a new
cycle cut from a trunk that still lacks a merged sync PR re-includes
that PR's whole delta — the "why so many changes" surprise. The guard
offers to squash-merge the open PR first (fast-forwarding the local
trunk so the new cycle cuts clean), continue anyway, or cancel. A PR
GitHub refuses to merge (conflicts / failing required checks) stops the
run with guidance. Degrades to a no-op without `gh`, with no open sync
PR, or in a non-interactive session.

--direct-merge: once the sync PR is open, enable GitHub auto-merge
(squash) so it merges itself when required checks pass. Opt-in per run;
release-please is unaffected (it watches the trunk for the one squashed
`chore:` commit either way).

New `src/utils/gh.ts` consolidates the `gh` wrappers (list/merge/close)
behind pure, unit-tested helpers (`filterSyncPrs`, `buildMergeArgs`).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@flipvh flipvh merged commit 9a7411c into main Jul 9, 2026
6 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant